/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package databook;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.FileReader;

import prefuse.data.Table;
import prefuse.data.io.CSVTableReader;
import prefuse.data.io.DataIOException;

/**
 *
 * @author zach
 */
public class CSVReader extends CSVTableReader {
    
    private File datafile;
    private int maxRecords = -1;
    private String[] columns;
    
    public CSVReader(String filename, int max) {
        this(filename,max,null);
    }
    
    public CSVReader(String filename, String[] col) {
        this(filename,-1,col);
    }
    
    public CSVReader(String filename) {
        this(filename,-1,null);
    }
    
    public CSVReader(String filename,int max, String[] col) {
        datafile = new File(filename);
        setColumns(col);
        setMaxRecords(max);
    }
    
    public void setColumns(String[] col) {
        columns = col;
    }
    
    public void setMaxRecords(int max) {
        maxRecords = max;
    }
    
    public int getMaxRecords() {
        return maxRecords;
    }
    
    // Returns yes if all the data is needed, meaning a straight up
    // CSV reading.  Otherwise, we will need to be selective about
    // what we read in.  We do this here instead of afterwards because
    // the file might be too large to fit into memoery
    private boolean allData() {
        return ((columns == null || columns.length == 0) && (maxRecords == -1));
    }
    
    public Table parseTable() throws DataIOException, FileNotFoundException {
        // If it needs all the data, then just use the version implemented by prefuse
        // which is a superclass of this one.
        if (allData()) {
            return readTable(datafile);
        }
        
        Table table = new Table();
        
        try {
            BufferedReader reader = new BufferedReader(new FileReader(datafile));
            String[] headers = reader.readLine().split(",");
            int[] colNums = new int[headers.length];
            if (columns != null) {
                int index = 0;
                boolean found;
                int j;
                for (int i = 0; i < columns.length; i++) {
                    for (j = 0, found = false; j < headers.length && !found; j++) {
                        if (columns[i].equals(headers[j])) {
                            colNums[index] = j;
                            index++;
                            found = true;
                        }
                    }
                }
                if (colNums.length != index) {
                    int[] newColNums = new int[index];
                    for (int i = 0; i < index; i++) {
                        newColNums[i] = colNums[i];
                    }
                    colNums = newColNums;
                }
            } else {
                for (int i = 0; i < colNums.length; i++) {
                    colNums[i] = i;
                }
            }
            
            for(int i = 0; i < colNums.length; i++) {
                table.addColumn(headers[colNums[i]],String.class);
            }
            
            // colNums should all be set up an initialized regardless of the configuration
            String input = "";
            while(((input = reader.readLine()) != null) && (maxRecords == -1 || table.getTupleCount() < maxRecords)) {
                // Might be simplistic.  Could have escaped commas.
                //String[] cols = input.split(",");
                String[] cols = readLine(input,colNums);
                int row = table.addRow();
                for(int i = 0; i < cols.length; i++) {
                    table.setString(row,i,cols[i]);
                    //Should be equivalent
                    //table.setString(row,headers[colNums[i]],cols[colNums[i]]);
                }
                
            }
            return table;
            
        } catch (FileNotFoundException fnfe) {
            // Might not need this clause.  But I seem to remember that if you put
            // FileNotFoundException after IOException, it's never caught, meaning
            // I think IOException catches FileNotFoundException.  But I want to leave
            // the option for the application to handle both sepatately.  
            throw fnfe;
        } catch (IOException ioe) {
            throw new DataIOException(ioe);
        }
    }
    
    public static String[] readLine(String line,int[] colNums) {
        StringBuffer sbuf = new StringBuffer();
        String[] parsed = new String[colNums.length];
        
        boolean inRecord = false;
        int inQuote  = 0;
        int col      = 0;
        int vcol     = 0;
            
        // extract the character array for quicker processing
        char[] c = line.toCharArray();
        int last = c.length-1;

        // iterate through current line
        for ( int i=0; i<=last; ++i ) {
            if ( !inRecord ) {
                // not currently processing a record
                if ( Character.isWhitespace(c[i]) )
                {
                    continue;
                }
                else if ( c[i] == '\"' )
                {
                    inRecord = true;
                    inQuote  = 1;
                }
                else if ( c[i] == ',' )
                {
                    String s = sbuf.toString().trim();
                    for (int ind = 0; ind < colNums.length; ind++) {
                        if (col == colNums[ind]) {
                            parsed[ind] = s;
                            break;
                        }
                    }
                    col++;
                    sbuf.delete(0, sbuf.length());
                }
                else
                {
                    inRecord = true;
                    sbuf.append(c[i]);
                }
            } else {
                // in the midst of a record
                if ( inQuote == 1 ) {
                    if ( c[i]=='\"' && (i==last || c[i+1] != '\"') )
                    {
                        // end of quotation
                        inQuote = 2;
                    }
                    else if ( c[i]=='\"' )
                    {
                        // double quote so skip one ahead
                        sbuf.append(c[i++]);
                    }
                    else
                    {
                        sbuf.append(c[i]);
                    }
                } else {
                    if ( Character.isWhitespace(c[i]) )
                    {
                        sbuf.append(c[i]);
                    }
                    else if ( c[i] != ',' && inQuote == 2 )
                    {
                        throw new IllegalStateException(
                            "Invalid data format. " + 
                            //"Error at line " + lineno + ", col " + i);
                            "Error at, col " + i + " for line " + line);
                    }
                    else if ( c[i] != ',' )
                    {
                        sbuf.append(c[i]);
                    }
                    else
                    {
                        String s = sbuf.toString().trim();
                        for (int ind = 0; ind < colNums.length; ind++) {
                            if (col == colNums[ind]) {
                                parsed[ind] = s;
                                break;
                            }
                        }
                        col++;
                        sbuf.delete(0, sbuf.length());
                        inQuote = 0;
                        inRecord = false;
                    }
                }
            }
        }
        if ( inQuote != 1 ) {
            String s = sbuf.toString().trim();
            for (int ind = 0; ind < colNums.length; ind++) {
                if (col == colNums[ind]) {
                    parsed[ind] = s;
                    break;
                }
            }
            col++;
            sbuf.delete(0, sbuf.length());
            inQuote = 0;
            inRecord = false;
        }
        if ( !inRecord && col > 0 ) {
            col = 0;
        }
        return parsed;
    }
}
